iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
Modern Web

從 React 學 Next.js:不只要會用,還要真的懂系列 第 27

【Day 27】用 Next.js 進行 SEO:Meta 設定與常見誤區

  • 分享至 

  • xImage
  •  

說到 Next.js,除了檔案系統的路由設定很便利外,應該還會想到「對於 SEO 很友善」的這部分。今天就讓我們來看看 Next.js 究竟為什麼會大家當做寫 React 時,要強化 SEO 的首選。

什麼是 SEO?

SEO 是 Search Engine Optimization 的縮寫,意思是就是「搜尋引擎優化」。對網站進行 SEO 的主要目的就是讓網站的能見度更高,更容易顯示在搜尋引擎中,換句話說,也就是更容易讓使用者透過搜尋引擎搜尋到。

當使用者在搜尋引擎用關鍵字進行搜尋時,搜尋引擎會把之前爬蟲爬過,並且建立好的索引,以演算法排序好對應內容,再返回給使用者。所以以提供網頁方的角度,就是要盡可能讓網站內容設計到可以讓爬蟲爬到大家很常會用來搜尋的關鍵字。

SEO 的重點在於

我們已經知道 SEO 主要目的在於讓爬蟲爬到關鍵字,那這個部分要怎麼做到呢?主要的重點就在於:

- 讓關鍵內容預渲染在 HTML(SSG/SSR/ISR):這部分是要讓標題、摘要、主要內文與內部連結直接出現在初始 HTML。若原始碼沒有關鍵內容,必須在瀏覽器上額外透過執行 JavaScript 才能取得詳細資訊,爬蟲就比較難爬取得重點資訊。
- 正確使用語意化標籤:正確使用 HTML 標籤(例如: h1、h2、footer、nav 等)能讓爬蟲不只抓取到資訊,也能讀懂資訊。
- 優化網頁加載速度:加快網頁的載入速度,能讓使用者體驗變好,魷魚使用者體驗也會被 google 搜尋引擎當作排名的參考,所以能更有機會讓網站排名比較前面,除此之外,也能間接地讓爬蟲能更穩定地爬取資訊。
- 優化網站的使用者體驗:Google 搜尋引擎在將網站進行排名時,會將網站使用者體驗的部分放在評估的項目中,所以優化使用者體驗也能改善網站的排名,讓自己的網站能更有效率地曝光給使用者。

用一句話說明重點的話,就是讓網頁的內容「能被抓取、看得懂、對使用者有價值、體驗良好」。

React vs Next.js 的 SEO?

看了 SEO 的重點內容,緊接著來看看當我們使用 React 或 Next.js 開發網頁時,要如何做到 SEO 的部分。

首先,來看看我們這次的第二男主角 - React
由前面我們已經看過的內容下去思考的話,我們可以知道 React 在 SEO 的部分,會有一個很大的弱點:「如果搜尋引擎沒有執行 JavaScript 的能力,就沒辦法在第一時間取得並讀懂網站的內容」。因為 React 的渲染模式是 CSR,所以進到瀏覽器時,一定會需要執行 JavaScript 才能取得頁面的詳細內容。如果瀏覽器的爬蟲,沒有執行 JavaScript 的能力,就無法讓爬蟲穩定抓取網站內容。不過近年來,Google 及 Bing 搜尋引擎都有執行 JavaScript 的能力,所以即使使用純的 React 進行網頁的開發,仍然可以讓爬蟲抓取得到對應的內容,只是以穩定性來說,還是沒有這麼好。

再來是我們的第一男主角 - Next.js
Next.js 主要提供的渲染模式是 SSG、ISR 及 SSR 這三種渲染模式都可以讓伺服器返回完整的 HTML 給瀏覽器,所以在讓網頁能被抓取的部分上是非常有幫助的。除此之外,Next.js 也提供了一些優化網站效能的能力,像是前面有陸續提到過的 Steaming、Image、next/dynamic 等,都有助於讓網站能更全面地做好 SEO 的部分,讓網站的排名變得更好。除此之外,在 Next.js 框架中,可以依照不同的 route 設定專屬的 <title><meta> 標籤,讓搜尋引擎能更準確地讀取網頁資訊。在 React 中,則需要另外安裝套件,才可以依照 route 個別設定 meta 相關的資訊。

在 Next.js 中定義 Route 的 Meta 資訊

前幾天我們已經看過不少 Next.js 提供的效能優化方式,這些功能可以間接提升 SEO 分數,提高網站的排名。接下來讓我們把焦點放回最基礎、也是讓搜尋引擎與社群平台能正確讀懂頁面資訊的關鍵:<meta><title> 標籤設定。

Page Router
在 Page Router 中,需要透過 import <Head> 元件來設定 title 和 meta 的內容。

// src/pages/about/index.tsx
import Head from "next/head";

export default function AboutPage() {
  return (
    <>
      <Head>
        <title>關於我們 | My Website Page</title>
        <meta
          name="description"
          content="這是關於我們的介紹頁面,透過這個頁面你可以更了解我們。"
        />
        <meta property="og:title" content="關於我們" />
        <meta property="og:description" content="更多關於我們的資訊。" />
        <meta property="og:image" content="https://example.com/og-image.jpg" />
      </Head>
      <h1>About Page</h1>
    </>
  );
}

這樣設定完後,位在對應 route 的頁面時,就可以看到顯示設定的 title 和 meta 內容。
https://ithelp.ithome.com.tw/upload/images/20250929/201309147gaL5qlJxr.png

如果需要在 title 或 meta 設定動態的資訊,可以在透過 getStaticProps 取得 fetch 到的資訊後,透過 Props 帶入 title 和 meta 上。
例如以下這個寫法:

// src/pages/about/index.tsx
export default function AboutPage({info}) {
  return (
    <>
      <Head>
        <title>{info.title}</title>
        <meta
          name="description"
          content={info.description}
        />
        <meta property="og:title" content={info.info} />
        <meta property="og:description" content={info.description} />
        <meta property="og:image" content="https://example.com/og-image.jpg" />
      </Head>
      <h1>About Page</h1>
    </>
  );
}

App Router
在 Next.js 的 App Router 中,設定 title 和 meta 的方式與 Page Router 不同,在 App Router 中,可以用一個類似 config 的方式來定義 meta 和 title 的內容。

如果只是要設定靜態的 meta 資訊,可以這樣寫。

// src/app/about/page.tsx
export const metadata = {
  title: "關於我們 | My Website Page",
  description: "這是關於我們的介紹頁面,透過這個頁面你可以更了解我們。",
  openGraph: {
    title: "關於我們",
    description: "更多關於我們的資訊。",
    images: ["https://example.com/og-image.jpg"],
  },
};

export default function AboutPage() {
  return <h1>About Page</h1>;
}

這樣點從 HTML 就可以看到相關的設定,而且在 App router 中,如果沒有額外定義 twitter 的內容,也會很聰明地使用 openGraph 的內容當作 twitter 對應內容的預設值。
https://ithelp.ithome.com.tw/upload/images/20250929/20130914Oxd6g3GzqI.png

如果需要使用動態的資訊來定義 meta,可以使用 generateMetadata 來 fetch 資料,並把他使用在 title 和 meta 內容中。

在 App Router 中,有別於 Page Router,需要先用 getStaticProps fetch 資料後,再透過 props 把資料帶進去元件中,才能用在 meta 和 title 的定義上。App Router 是直接透過 generateMetadata 把 fetch 資料和使用動態資料定義 meta 和 title 的動作一起進行。

// src/app/about/page.tsx
import type { Metadata, ResolvingMetadata } from 'next'
 
type Props = {
  params: Promise<{ id: string }>
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}
 
export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  const id = (await params).id
 
  // fetch info data
  const info = await fetch(`https://example/${id}`).then((res) =>
    res.json()
  )
 
  return {
    title: info.title,
    description: info.description,
  }
}
 
export default function Page({ params, searchParams }: Props) {}

SEO 誤區:dynamic 使用錯誤及 CSR 思維

前面我們已經看過在 Next.js 中如何設定 title 與 meta,讓我們再接著看看關於 SEO 的誤區。

我們已經知道提升網頁效能、設定正確的 meta 與 title、把重要資訊放在首屏,這些做法都能幫助 SEO。
但問題是「當我們把所有方法一次用上,真的會對 SEO 更有幫助嗎?」

以之前我們看過的 next/dynamic 為例,它能將 bundle 拆小,減少 JavaScript 一次載入的阻塞,以提升效能,並間接對 SEO 有幫助。
「若是某些區塊內容因此被延後載入,搜尋引擎爬蟲是否就無法在第一時間抓取到?」

還有當我們習慣用 CSR 思維下去進行頁面的開發,透過 useEffect fetch 資料,「雖然可以透過一些標示處理 loading 中的畫面,但是這樣的做法對 SEO 是有幫助的嗎?」

接下來我們就模擬一個我實際實作過的例子,並且透過 App Router 的寫法,來看看這個部分。

這個例子是一個會顯示部落格內文的頁面,目標就是要讓 SEO 的效果能達到最佳化。

如果不跳脫 CSR 的開發思維可能就會是在元件內透過 useEffect 去處理 fetch 資料的動作,然後發現這個區塊出現的時間會比較久一點,甚至有可能會想要用 dynamic 去做 code Splitting。

// 元件的部分
"use client";

import { Post } from "@/app/api/blogs/[id]/route";
import { useEffect, useState } from "react";

interface BlogArticleClientProps {
  id: string;
}
const BlogArticleClient = ({ id }: BlogArticleClientProps) => {
  const [post, setPost] = useState<Post | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    async function fetchPost() {
      try {
        const res = await fetch(`/api/blogs/${id}`);
        if (!res.ok) {
          throw new Error("Failed to fetch post");
        }
        const data = await res.json();
        setPost(data);
      } catch (err) {
        console.error(err);
      } finally {
        setIsLoading(false);
      }
    }

    fetchPost();
  }, [id]);

  if (isLoading) {
    return <p>Loading...</p>;
  }

  return (
    <article>
      <h1 className="text-2xl font-bold mb-4">{post?.title}</h1>
      <p className="mb-2">
        <em>{post ? new Date(post?.createdTime).toLocaleString() : "--"}</em>
      </p>
      <div>{post?.content}</div>
    </article>
  );
};

export default BlogArticleClient;

// 使用時
import dynamic from "next/dynamic";

const BlogArticleClient = dynamic(
  () => import("./_components/BlogArticleClient")
);

interface Post {
  id: string;
  title: string;
  description: string;
  content: string;
  createdTime: string;
}

export default async function BlogPostPage({
  params,
}: {
  params: { id: string };
}) {
  const { id } = await params;
  return <BlogArticleClient id={id} />;
}

呈現的狀況就會是這個樣子。
https://i.imgur.com/QoVzcci.gif

看起來好像沒什麼奇怪的地方,因為當資料還沒有回來時,我們有告訴使用者目前正在 loading 中,我們甚至還有用 dynamic,但是檢視程式原始碼會發現「完蛋!我的重點資訊怎麼都沒出現?」
https://ithelp.ithome.com.tw/upload/images/20250929/20130914Rm4xgYfe9I.png

這裡的第一個問題是 沒有跳脫 CSR 思維
我們需要記得的是並不是使用 Next.js 就一定會是 SSR、SSG 這兩個渲染模式,還要注意我們是不是使用 Client Component。雖然 Client Component 會在 Server 上執行,但是 React hook 只會在 client 端上被執行,意思也就是在 useEffect 上 fetch 和畫面渲染相關的資料的動作是在 client 端上進行,第一時間從伺服器 response 的 HTML 就一定不會完整的畫面內容。

再來第二個問題是 我們可以使用 dynamic 讓這個區塊晚點被載入,但是在上述的情境下不需要額外使用 dynamic
雖然如果變成寫死的文字內容,並不會對 SEO 造成負面影響,也不會有正面的影響,因為使用 dynamic 沒有額外加上其他設定時,預設還是會在伺服器上進行畫面的渲染(顯示預設寫死的內容),但如果不小心寫上 SSR: false 這個設定的話,就會對 SEO 造成負面影響,因為這樣設定等於略過了先在伺服器渲染的步驟,即使都是靜態的內容,也沒辦法先在伺服器上被渲染出來,這樣反而會造成反效果。

既然我們都已經用 Next.js 這套框架了,在處理這個非常需要 SEO 的頁面時,其實我們有更聰明的做法,那就是跳脫 CSR 思維,讓 fetch 資料的動作在伺服器上進行。

import BlogArticle from "./_components/BlogArticle";

async function getPost(id: string): Promise<Post> {
  const res = await fetch(`http://localhost:3000/api/blogs/${id}`, {
    cache: "no-store",
  });

  if (!res.ok) {
    throw new Error("Failed to fetch post");
  }

  return res.json();
}

export default async function BlogPostPage({
  params,
}: {
  params: { id: string };
}) {
  const { id } = await params;
  const post = await getPost(id);

  return (
    <BlogArticle post={post} />
  );
}

當我們改成這樣的寫法後,實際看畫面就可以發現到,其實根本不需要顯示 loading 的標示,因為畫面會在伺服器上完整產出後再 response 回來,也就不會在畫面上長時間出現空白的情況。
https://i.imgur.com/1jm2rob.gif

總結

Next.js 相較於純 React 更適合進行 SEO,因為 Next.js 提供 SSR、SSG、ISR 這些能讓爬蟲第一時間就抓到完整 HTML 的渲染模式,而 SEO 的本質也就是讓網頁內容「能被抓取、看得懂、對使用者有價值、體驗良好」。由於 Next.js 除了能優化效能(如 Image、dynamic import、Streaming),還能針對不同路由方便地設定 與 ,所以對於 SEO 能有更加分。但是並不是使用 Next.js 進行專案開發,就等於有做 SEO,如果用了 Next.js 但還是侷限於 CSR 思維(例如用 useEffect 在 Client Component 裡 fetch 資料),那就會導致爬蟲看到的初始 HTML 是空的,或錯誤地使用 dynamic 搭配 ssr: false,也會讓真正重要的內容無法在第一時間顯示出來,這樣其實反而會對 SEO 造成負面影響。想要做好 SEO 還是要善用 Next.js 的伺服器端能力,並且依照實際狀況進行合適的優化手段,才能將 Next.js 對於 SEO 的優勢發揮到最大。


以上就是我自己個人從寫純 React 轉換到 Next.js 踩到過的未跳脫 CSR 思維,以及以為只要有用 dynamic 就好,沒有好好去思考為什麼要使用所造成的 SEO 誤區。

那今天的主題就到這裡告一個段落,我們明天見!

參考資料

官方文件 - Metadata and SEO
官方文件 - Metadata and OG images


上一篇
【Day 26】Next.js 的 JavaScript 載入優化方式 - next/dynamic
下一篇
【Day 28】Next.js 是成為全端框架?!建立 API 和直接接觸後端 DB 的方式
系列文
從 React 學 Next.js:不只要會用,還要真的懂30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言